package stringdecrypter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ArrayType;
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.ConstantPushInstruction;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.LDC;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.TABLESWITCH;
import org.apache.bcel.generic.TargetLostException;
import org.apache.bcel.generic.Type;
import org.apache.bcel.util.InstructionFinder;

public class for651 {
    
    public static boolean verbose = true;
    
    public static void main(final String[] args) throws IOException {
        final JarFile file = new JarFile(new File("archive/cached/cached_latest.jar"));
        
        final ArrayList<ClassGen> classes = new ArrayList<ClassGen>();
        
        final Enumeration<JarEntry> entries = file.entries(); // gets all the files within the jar file
        while (entries.hasMoreElements()) // Checks to see if there are any other files in the jar
        {
            final JarEntry entry = entries.nextElement(); // Gets the next file in the jar
            if (entry.getName().endsWith(".class")) // Checks to make sure it is a class file
            {
                final InputStream is = file.getInputStream(entry); // Stores the contents of the file in a stream
                final ClassParser parser = new ClassParser(is, entry.getName());
                final JavaClass jc = parser.parse(); // Parses the class file
                classes.add(new ClassGen(jc)); // Creates the class gen to be stored for later use
            }
        }
        
        for (final ClassGen cg : classes) {
            fixStringEncryption(cg);
        }
        
        final JarOutputStream jos = new JarOutputStream(new FileOutputStream(new File("decrypted_cached_latest.jar")));
        
        for (final ClassGen cg : classes) {
            final JarEntry je = new JarEntry(cg.getClassName() + ".class");
            jos.putNextEntry(je);
            
            jos.write(cg.getJavaClass().getBytes());
            jos.flush();
            
            jos.closeEntry();
        }
        jos.close();
        
    }

    //Speed's client string decryption
    public static int[] getKeys(final Method m) {
        final String pattern = "tableswitch ConstantPushInstruction goto ConstantPushInstruction goto ConstantPushInstruction goto ConstantPushInstruction goto ConstantPushInstruction ixor";
        final InstructionFinder find = new InstructionFinder(new InstructionList(m.getCode().getCode()));
        final int[] keys = new int[5];
        final Iterator<InstructionHandle[]> it = find.search(pattern);
        int i = 0;
        while (it.hasNext()) {
            final InstructionHandle[] handles = it.next();
            final TABLESWITCH tblswitch = (TABLESWITCH) handles[0].getInstruction();
            for (final InstructionHandle handle : tblswitch.getTargets()) {
                final Instruction ins = handle.getInstruction();
                if (ins instanceof ConstantPushInstruction) {
                    keys[i++] = ((ConstantPushInstruction) ins).getValue().intValue();
                }
            }
            final Instruction o = tblswitch.getTarget().getInstruction();
            if (o instanceof ConstantPushInstruction) {
                keys[i++] = ((ConstantPushInstruction) o).getValue().intValue();
            }

            if (verbose) {
                System.out.println(Arrays.toString(keys));
            }
        }
        return keys;
    }

    private static final int getKey(final Method m) {
        final String pattern = "caload ConstantPushInstruction ixor i2c";
        final InstructionFinder finder = new InstructionFinder(new InstructionList(m.getCode().getCode()));
        final Iterator<InstructionHandle[]> it = finder.search(pattern);
        while (it.hasNext()) {
            final InstructionHandle handle = it.next()[1];
            final Instruction i = handle.getInstruction();
            if (i instanceof ConstantPushInstruction)
                return ((ConstantPushInstruction) i).getValue().intValue();
        }
        return -1;
    }

    /**
     * Speed's method for decrypting Jagex's strings in runescape client 651.
     *
     */
    public static void fixStringEncryption(final ClassGen cg) {
        int[] keys = null;
        int key = -1;
        final ConstantPoolGen cpg = cg.getConstantPool();
        for (final Method m : cg.getMethods()) {
            if (m.isPrivate() && m.isStatic() && m.getReturnType().equals(Type.STRING) && m.getArgumentTypes()[0].equals(new ArrayType(Type.CHAR, 1))) {
                //we have found the string encryption
                keys = getKeys(m);
            } else if (m.isPrivate() && m.isStatic() && m.getReturnType().equals(new ArrayType(Type.CHAR, 1)) && m.getArgumentTypes()[0].equals(Type.STRING)) {
                key = getKey(m);
            }
        }
        if (key != -1 && keys != null) {
            for (final Method m : cg.getMethods()) {
                if (m.getName().equals("<clinit>")) {
                    final MethodGen mg = new MethodGen(m, cg.getClassName(), cg.getConstantPool());
                    final String pattern = "ldc invokestatic invokestatic aastore";
                    final InstructionList list = mg.getInstructionList();
                    final InstructionFinder finder = new InstructionFinder(list);
                    final Iterator<InstructionHandle[]> it = finder.search(pattern);
                    while (it.hasNext()) {
                        final InstructionHandle[] handles = it.next();
                        final LDC ldc = (LDC) handles[0].getInstruction();
                        final String val = ldc.getValue(cpg).toString();
                        final String dec = decrypt(decrypt(val, key), keys);
                        if (verbose) {
                            System.out.println(dec);
                        }
                        //System.out.println(val + " --> " + dec);
                        if (cpg.lookupString(val) > -1) {
                            final int i = cpg.addString(dec);
                            cpg.setConstant(cpg.lookupString(val), cpg.getConstant(i));
                        }
                        if (cpg.lookupUtf8(val) > -1) {
                            final int i = cpg.addUtf8(dec);
                            cpg.setConstant(cpg.lookupUtf8(val), cpg.getConstant(i));
                        }
                        try {
                            list.delete(handles[1]);
                            list.delete(handles[2]);
                        } catch (final TargetLostException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    private static char[] decrypt(final String string, final int key) {
        final char[] cs = string.toCharArray();
        if (cs.length < 2) {
            cs[0] ^= (char) key;
        }
        return cs;
    }

    private static String decrypt(final char[] input, final int[] keys) {
        int index = 0;
        for (; ; ) {
            if (input.length <= index) {
                break;
            }
            final char c = input[index];
            char xor;
            switch (index % 5) {
                case 0:
                    xor = (char) keys[0];
                    break;
                case 1:
                    xor = (char) keys[1];
                    break;
                case 2:
                    xor = (char) keys[2];
                    break;
                case 3:
                    xor = (char) keys[3];
                    break;
                default:
                    xor = (char) keys[4];
            }
            input[index] = (char) (c ^ xor);
            index++;
        }
        final String string = new String(input);
        return string.intern();
    }
}
